package jeudevie.models;

import java.io.File;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.Set;

import javax.management.modelmbean.XMLParseException;


/**
 * Une grille bidimensionnelle contenant la population des cellules
 *
 * La population est representee comme une hashmap contenant les cellules
 * vivantes.
 *
 * Le choix d'une LinkedHashMap au lieu de HashMap est justifie par la necessite
 * d'iterrer dans la liste de cles a chaque changement d'etat Operation en
 * O(<nbre elements>) dans une LinkedHashMap, mais en O(<capacite>) dans une
 * HashMap
 */
public class Population extends Grid2d<Cell> {

    /**
     * utilise pour stoquer le nombre de voisins pour passer a l'etat suivant de
     * la population. Deffinit en variable d'objet pour ne pas allouer la
     * memoire a chaque appel de la methode nextState
     */
    private LinkedHashMap<Position, Integer> nbNeighMap;
    /**
     * utilise pour calculer le nouvel etat de la population. En variable
     * d'objet pour la meme raison que nbNeighMap
     */
    private LinkedHashMap<Position, Cell> newGrid;

    /**
     * Creer une polulation de taille size * size
     *
     * @param size la taille de la population
     */
    public Population(int size) {
        this(size,size);
    }

    public Population(int sizeX, int sizeY) {
        super(sizeX, sizeY);
        this.setGrid(new LinkedHashMap<Position, Cell>(sizeX * sizeY));
        this.nbNeighMap = new LinkedHashMap<Position, Integer>(sizeX*sizeY);
        this.newGrid = new LinkedHashMap<Position, Cell>(sizeX*sizeY);
    }

    public void resize(int sizeX, int sizeY) {

        /*
         * supprimer les elements si la nouvelle tailleX est plus petite
         */
        if (this.getSizeX() > sizeX) {
            for (int i = sizeX; i < this.getSizeX(); i++) {
                for (int j = 0; j < this.getSizeY(); j++) {
                    this.getGrid().remove(new Position(i, j));
                }
            }
        }
        this.setSizeX(sizeX);

        /*
         * supprimer les elements si la nouvelle tailleY est plus petite
         */
        if (this.getSizeY() > sizeY) {
            for (int i = 0; i < this.getSizeX(); i++) {
                for (int j = sizeY; j < this.getSizeY(); j++) {
                    this.getGrid().remove(new Position(i, j));
                }
            }
        }
        this.setSizeY(sizeY);

        this.setChanged();
        this.notifyObservers();
    }

    public synchronized void empty() {
        this.getGrid().clear();
        this.setChanged();
        this.notifyObservers();
    }

    @Override
    public synchronized void setValue(Position pos, Cell val) {
        this.getGrid().put(pos, val);
        this.setChanged();
    }

    @SuppressWarnings("unchecked")
    @Override
    public LinkedHashMap<Position, Cell> getGrid() {
        return (LinkedHashMap<Position, Cell>) super.getGrid();
    }

    public Cell.State getCellState(Position pos) {
        if (this.getGrid().containsKey(pos)) {
            return Cell.State.ALIVE;
        } else {
            return Cell.State.DEAD;
        }
    }

    /**
     * Changer l'etat de la cellule a la position pos
     * si elle est vivante, la detruire, si elle est morte,
     * faire naitre une nouvelle cellule
     *
     * @param pos la position de la cellule qui doit changer d'etat
     */
    public synchronized void changeCellState(Position pos) {
        if (this.getGrid().containsKey(pos)) {
            this.getGrid().remove(pos);
        } else {
            this.getGrid().put(pos, new Cell(this));
        }
        this.setChanged();
        this.notifyObservers();
    }

    /**
     * Changer l'etat de la cellule a la position pos:
     * si elle est morte, faire naitre une nouvelle cellule
     *
     * @param pos la position de la cellule qui doit changer d'etat
     */
    public synchronized void awakeCellAt(Position pos) {
        if (! this.getGrid().containsKey(pos)) {
            this.getGrid().put(pos, new Cell(this));
        }
        this.setChanged();
        this.notifyObservers();
    }

    /**
     * Changer l'etat de la cellule a la position pos:
     * si elle est vivante, retirer la cellule
     *
     * @param pos la position de la cellule qui doit changer d'etat
     */
    public synchronized void removeCellAt(Position pos) {
    	if (this.getGrid().containsKey(pos)) {
            this.getGrid().remove(pos);
        } 
        this.setChanged();
        this.notifyObservers();
    }

    /**
     *
     * @return l'ensemble des cellules vivantes
     */
    public Set<Position> getLivingCells() {
        return this.getGrid().keySet();
    }

    public synchronized void nextState() {
        this.nbNeighMap.clear();
        this.newGrid.clear();

        Set<Position> livingCells = this.getLivingCells();
        Iterator<Position> itLivingCells = livingCells.iterator();

        /*
         * pour les Cellules qui peuvent etre vivantes a la prochaine iteration
         * (les cellules actuellement vivantes et ses voisines) compter le
         * nombre de voisins et le stoquer dans nbNeighbour
         *
         * L'algorythme pour atteindre ce but: pour chaque cellule actuelement
         * vivante, on ajout +1 au nbNeighbour de chaqun de ses voisins.
         */
        while (itLivingCells.hasNext()) {
            Position pos = itLivingCells.next();
            ArrayList<Position> neighboursList = this.getNeighboursList(pos);

            Iterator<Position> itNeighbours = neighboursList.iterator();
            while (itNeighbours.hasNext()) {
                Position posVois = itNeighbours.next();
                if (!this.nbNeighMap.containsKey(posVois)) {
                    this.nbNeighMap.put(posVois, 1);
                } else {
                    this.nbNeighMap.put(posVois, this.nbNeighMap.get(posVois) + 1);
                }
            }
        }

        /*
         * trouver les Cellules qui seront vivantes a la prochaine iteration et
         * les stocker dans newGrid : pour chque cellule qui a au moins 1 voisin
         * voir si elle sera vivante au prochain etat
         */

        Set<Position> keySet = this.nbNeighMap.keySet();
        Iterator<Position> it = keySet.iterator();
        while (it.hasNext()) {
            Position pos = it.next();
            int nbVois = this.nbNeighMap.get(pos);
            if (nbVois == 1 || nbVois > 3) {
                continue;
            }
            if (this.getGrid().containsKey(pos)) {
                this.newGrid.put(pos, this.getGrid().get(pos));
            } else {
                if (nbVois == 3) {
                    this.newGrid.put(pos, new Cell(this));
                }
            }
        }

        LinkedHashMap<Position, Cell> temp = this.getGrid();
        this.setGrid(this.newGrid);
        this.newGrid = temp;
        
        this.setChanged();
        this.notifyObservers();
    }
    
    /**
     * Sauvegarder la population dans un fichier
     * @param fileName le nom du fichier
     */
    public void save(String fileName) {
        new Backup().save(this,new File(fileName));
    }
    
    /**
     * Recuperer la population depuis un fichier 
     * @param fileName le nom du fichier
     */
    public void load(String fileName) throws XMLParseException {
        new Backup().load(this,new File(fileName));
        this.setChanged();
        this.notifyObservers();
    }

    /**
     * La main utilise pour les test de l'objet
     * @param args 
     */
    public static void main(String[] args) {
        Population pop = new Population(6);

        pop.setValue(new Position(1, 1), new Cell(pop));
        pop.setValue(new Position(1, 2), new Cell(pop));
        pop.setValue(new Position(2, 1), new Cell(pop));
        pop.setValue(new Position(2, 2), new Cell(pop));
        pop.setValue(new Position(3, 3), new Cell(pop));
        pop.setValue(new Position(3, 4), new Cell(pop));
        pop.setValue(new Position(4, 3), new Cell(pop));
        pop.setValue(new Position(4, 4), new Cell(pop));

        // new Sauvegarde().save(pop);

        for (int k = 0; k < 5; k++) {
            pop.nextState();
        }

    }
}
